package com.test.utils;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import com.test.exceptions.RouteDataException;
import com.test.models.City;
import com.test.models.Route;
import com.test.storages.DataBase;

/**
 * RouteUtils
 * 
 * @author ezuccon
 * @category Utils
 * @version 1.0
 * @since 12/03/2013
 */
public class RouteUtils {
	/**
	 * Constructor oculto
	 */
	protected RouteUtils() {
		super();
	}

	/**
	 * Obtiene el nombre de la ciudad para mostrar
	 * 
	 * @param cityValue
	 * @return
	 */
	public static final String getCityName(City cityValue) {
		// valida si existe ciudad
		if (cityValue == null) {
			throw new RouteDataException();
		}

		// valida el nombre de la ciudad
		if (StringUtils.isBlank(cityValue.getName())) {
			return KeyUtils.UNKNOWN_CITY;
		}

		// devuelve el nombre de la ciudad
		return cityValue.getName();
	}

	/**
	 * Genera el nomrbe completo de la ruta
	 * 
	 * @param routeValue
	 * @return
	 */
	public static final String getRouteFullName(Route routeValue) {
		return (RouteUtils.getCityName(routeValue.getFrom())) + "-"
				+ (RouteUtils.getCityName(routeValue.getTo()));
	}

	/**
	 * Genera solo el nombre de origen de la ruta
	 * 
	 * @param routeValue
	 * @return
	 */
	public static final String getRouteFromName(Route routeValue) {
		return (RouteUtils.getCityName(routeValue.getFrom())) + "-";
	}

	/**
	 * Genera solo el nombre de destino de la ruta
	 * 
	 * @param routeValue
	 * @return
	 */
	public static final String getRouteToName(Route routeValue) {
		return "-" + (RouteUtils.getCityName(routeValue.getTo()));
	}

	/**
	 * Busca la ruta segun el codigo de entrada
	 * 
	 * @param dataBase
	 * @param route
	 */
	public static final List<City> findRoute(DataBase dataBase, String route) {
		// valida los datos de entrada
		if (StringUtils.isBlank(route)) {
			throw new RouteDataException();
		}

		// declara la lista de ciudades
		List<City> cities = new ArrayList<City>();

		// obtiene los nombres de ciudades
		String[] citiesNames = route.split(KeyUtils.ROUTE_NAME_SEPARATOR);

		// captura la lista de ciudades
		for (int i = 0; i != citiesNames.length; i++) {
			// busca la ciudad
			City city = DataBaseUtils.findCity(dataBase, new City(
					citiesNames[i]));

			// valida si existe la ciudad buscada
			if (city == null) {
				throw new RouteDataException();
			}

			// agrega la ciudad a la lista de rutas
			cities.add(city);
		}

		// devuelve la lista de ciudades de la ruta
		return cities;
	}

	/**
	 * Calcula la distancia completa de una ruta
	 * 
	 * @param dataBase
	 * @param route
	 * @return
	 */
	public static final int calculaRouteDistance(DataBase dataBase, String route) {
		// busca la ciudades de la ruta
		List<City> cities = RouteUtils.findRoute(dataBase, route);

		// declara el sumador de distancias
		int distance = 0;

		// itera sobre la lista de ciudades
		for (int i = 0; i != cities.size() - 1; i++) {
			distance += CityUtils.getDistance(cities.get(i), cities.get(i + 1));
		}

		// devuelve la distancia de la ruta
		return distance;
	}

	/**
	 * Obtiene los codigos de rutas de una ciudad a otra
	 * 
	 * @param dataBase
	 * @param from
	 * @param to
	 * @param maxStop
	 * @return
	 */
	public static final List<String> getRoutesByMaxStop(DataBase dataBase,
			String fromName, String toName, int maxStop) {
		// declara la lista inicial de codigos de ruta
		List<String> codes = RouteUtils.generateRouteCodes(dataBase,
				DataBaseUtils.getCitiesNames(dataBase));

		// itera sobre la lista de codigos
		for (int i = 0; i != maxStop; i++) {
			codes.addAll(RouteUtils.generateRouteCodes(dataBase, codes));
		}

		// filtra las rutas generadas
		return RouteUtils.filterRoutes(fromName, toName, codes);
	}

	/**
	 * Obtiene los codigos de rutas de una ciudad a otra
	 * 
	 * @param dataBase
	 * @param from
	 * @param to
	 * @param stop
	 * @return
	 */
	public static final List<String> getRoutesByStop(DataBase dataBase,
			String fromName, String toName, int stop) {
		// declara la lista inicial de codigos de ruta
		List<String> codes = RouteUtils.generateRouteCodes(dataBase,
				DataBaseUtils.getCitiesNames(dataBase));

		// itera sobre la lista de codigos
		for (int i = 0; i != stop; i++) {
			codes.addAll(RouteUtils.generateRouteCodes(dataBase, codes));
		}

		// filtra las rutas generadas
		return RouteUtils.filterRoutesBySize(fromName, toName, codes, stop);
	}

	/**
	 * Obtiene la distancia menor de ruta de un punto A a B
	 * 
	 * @param dataBase
	 * @param fromName
	 * @param toName
	 * @return
	 */
	public static final int getRoutesByMinDistance(DataBase dataBase,
			String fromName, String toName) {
		// declara la lista de rutas
		List<String> codes = new ArrayList<String>();
		int i = 0;

		// genera la lista de codigos
		while (codes.isEmpty()) {
			i++;
			codes.addAll(RouteUtils.getRoutesByMaxStop(dataBase, fromName,
					toName, i));
		}

		// declara la distancia minima
		int minDistance = -1;

		// itera sobre la lista de rutas
		for (String code : codes) {
			int routeDistance = RouteUtils.calculaRouteDistance(dataBase, code);
			if (minDistance < 0) {
				minDistance = routeDistance;
			} else {
				if (minDistance > routeDistance) {
					minDistance = routeDistance;
				}
			}
		}

		// devuelve la distancia menor para B a B
		return minDistance;
	}

	/**
	 * Obtiene la lista de rutas con una distancia mayor recibida por parametro
	 * 
	 * @param dataBase
	 * @param fromName
	 * @param toName
	 * @return
	 */
	public static final List<String> getRoutesByMaxDistance(DataBase dataBase,
			String fromName, String toName, int maxDistance) {
		// declara la lista de rutas
		List<String> codes = new ArrayList<String>();
		int i = 1;

		// genera la lista de codigos
		List<String> tempCodes = RouteUtils.getCodesByMaxDistance(dataBase,
				fromName, toName, i, maxDistance);

		// itera hasta encontra el primer tramo
		while (tempCodes.isEmpty()) {
			i++;
			tempCodes = RouteUtils.getCodesByMaxDistance(dataBase, fromName,
					toName, i, maxDistance);
		}

		// captura el offset de cierre
		int offset = tempCodes.get(0).length();

		// ejecuta mientras existan codigos
		while (!tempCodes.isEmpty()) {
			// carga los codigos obtenidos
			codes.addAll(tempCodes);

			// mueve el indice
			i++;

			// obtiene los nuevos codigos
			tempCodes = RouteUtils.getCodesByMaxDistance(dataBase, fromName,
					toName, i, maxDistance);

			// valida fin de ciclo
			if (tempCodes.isEmpty()) {
				for (int x = 0; x != offset; x++) {
					// mueve el indice
					i++;

					// obtiene los nuevos codigos
					tempCodes = RouteUtils.getCodesByMaxDistance(dataBase,
							fromName, toName, i, maxDistance);
					if (!tempCodes.isEmpty()) {
						// carga los codigos obtenidos
						codes.addAll(tempCodes);
						return codes;
					}
				}
			}
		}

		// devuelve la lista de rutas con distancia menor a la recibida por
		// parametro
		return codes;
	}

	/**
	 * Obtiene la lista de codigos que correspondan a una distancia maxima y
	 * paradas
	 * 
	 * @param dataBase
	 * @param fromName
	 * @param toName
	 * @param stop
	 * @param maxDistance
	 * @return
	 */
	public static final List<String> getCodesByMaxDistance(DataBase dataBase,
			String fromName, String toName, int stop, int maxDistance) {
		// declara la lista de rutas
		List<String> codes = new ArrayList<String>();

		// genera la lista de codigos
		List<String> tempCodes = RouteUtils.getRoutesByStop(dataBase, fromName,
				toName, stop);

		// itera sobre la lista de resultados obtenidos
		for (String code : tempCodes) {
			if (RouteUtils.calculaRouteDistance(dataBase, code) < 30) {
				codes.add(code);
			}
		}

		// devuele la lista de codigos
		return codes;
	}

	/**
	 * Obtiene la lista de codigos que correspondan a una distancia maxima y
	 * paradas
	 * 
	 * @param dataBase
	 * @param fromName
	 * @param toName
	 * @param stop
	 * @param maxDistance
	 * @return
	 */
	public static final List<String> getCodesFromMaxDistance(DataBase dataBase,
			String fromName, String toName, int stop, int maxDistance) {
		// declara la lista de rutas
		List<String> codes = new ArrayList<String>();

		// genera la lista de codigos
		List<String> tempCodes = RouteUtils.getRoutesByStop(dataBase, fromName,
				toName, stop);

		// itera sobre la lista de resultados obtenidos
		for (String code : tempCodes) {
			if (RouteUtils.calculaRouteDistance(dataBase, code) > 30) {
				codes.add(code);
			}
		}

		// devuele la lista de codigos
		return codes;
	}

	/**
	 * Genera una lista de rutas validas a partir de un codigo de ruta inicial
	 * 
	 * @param dataBase
	 * @param code
	 * @return
	 */
	public static final List<String> generateRouteCodes(DataBase dataBase,
			String code) {
		// genera la lista de nuevos codigos
		List<String> codes = new ArrayList<String>();

		// itera sobre la lista de ciudades
		for (City city : dataBase.getCities()) {
			// genera el nuevo codigo
			String newCode = code + KeyUtils.ROUTE_NAME_SEPARATOR
					+ city.getName();

			// valida el codigo
			try {
				RouteUtils.calculaRouteDistance(dataBase, newCode);
				codes.add(code + KeyUtils.ROUTE_NAME_SEPARATOR + city.getName());
			} catch (RouteDataException rde) {
			}
		}

		// devuelve la lista de rutas validas
		return codes;
	}

	/**
	 * Filtras las rutas por los extremos
	 * 
	 * @param fromFilterValue
	 * @param codes
	 * @return
	 */
	public static final List<String> filterRoutes(String fromFilterValue,
			String toFilterValue, List<String> codes) {
		// declara la lista de resultados
		Set<String> resultCodes = new HashSet<String>();

		// valida el inicio y fin de cada elemento
		for (String code : codes) {
			// captura los elementos por separado
			String[] subCodes = code.split(KeyUtils.ROUTE_NAME_SEPARATOR);

			// valida inicio y fin
			if ((subCodes[0].equals(fromFilterValue))
					&& ((subCodes[subCodes.length - 1].equals(toFilterValue)))) {
				resultCodes.add(code);
			}
		}

		// devuelve la lista de resultados
		return new ArrayList<String>(resultCodes);
	}

	/**
	 * Filtras las rutas por los extremos
	 * 
	 * @param fromFilterValue
	 * @param codes
	 * @return
	 */
	public static final List<String> filterRoutesBySize(String fromFilterValue,
			String toFilterValue, List<String> codes, int size) {
		// declara la lista de resultados
		Set<String> resultCodes = new HashSet<String>();

		// valida el inicio y fin de cada elemento
		for (String code : codes) {
			// captura los elementos por separado
			String[] subCodes = code.split(KeyUtils.ROUTE_NAME_SEPARATOR);

			// valida inicio y fin
			if ((subCodes[0].equals(fromFilterValue))
					&& ((subCodes[subCodes.length - 1].equals(toFilterValue)))
					&& (subCodes.length == (size + 1))) {
				resultCodes.add(code);
			}
		}

		// devuelve la lista de resultados
		return new ArrayList<String>(resultCodes);
	}

	/**
	 * Genera lista de codigos de ruta validados
	 * 
	 * @param dataBase
	 * @param codes
	 * @return
	 */
	public static final List<String> generateRouteCodes(DataBase dataBase,
			List<String> codes) {
		// declara la lista de nuevos codigos
		List<String> newCodes = new ArrayList<String>();

		// itera sobre la lista de codigos
		for (String code : codes) {
			// itera sobre la lista de ciudades
			for (City city : dataBase.getCities()) {
				// valida el codigo
				try {
					RouteUtils.calculaRouteDistance(dataBase, code
							+ KeyUtils.ROUTE_NAME_SEPARATOR + city.getName());
					newCodes.add(code + KeyUtils.ROUTE_NAME_SEPARATOR
							+ city.getName());
				} catch (RouteDataException rde) {
				}
			}
		}

		// devuelve la lista de codigos validos
		return newCodes;
	}
}
